﻿//Copyright 2011 - Jonathan Allen

using Granite.Modeling.Internals;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Granite.Collections;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using Granite.DataAnnotations;

#if SILVERLIGHT && !WINDOWS_PHONE
	using System.Collections;
#endif

namespace Granite.Modeling
{

	/// <summary>
	/// A base class for collections of models and entities.
	/// </summary>
	/// <typeparam name="T">The type of object being stored</typeparam>
	/// <typeparam name="TPropertyTracking">The type of property tracking desired.</typeparam>
	public abstract partial class ModelCollection<T, TPropertyTracking> : ExtendedObservableCollection<T>
		where TPropertyTracking : PropertyBagBase
	{
		readonly ErrorsDictionary m_Errors = new ErrorsDictionary();

		/// <summary>
		/// Backing store for properties
		/// </summary>
		readonly TPropertyTracking m_Properties;

		/// <summary>
		/// Raised when the the errors collection has changed. 
		/// </summary>
		/// <remarks>This may be fired even when no actual change has occurred.</remarks>
		public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

		void AttachPropertyEvents()
		{
			m_Properties.PropertyChanged += Properties_PropertyChanged;
			m_Properties.RevalidateProperty += (s, e) => ValidateProperty(e.PropertyName);
			m_Properties.RevalidateObject += (s, e) => ValidateObject();
		}

		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking.
		/// </summary>
		/// <remarks>Requires TPropertyTracking have a public constructor that accepts an Object</remarks>
		protected ModelCollection()
		{
			m_Properties = (TPropertyTracking)Activator.CreateInstance(typeof(TPropertyTracking), this);
			AttachPropertyEvents();
		}

		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking and populates it using the supplied list
		/// </summary>
		/// <param name="list">The list from which the elements are copied.</param>
		protected ModelCollection(List<T> list)
			: base(list)
		{
			Contract.Requires(list != null);

			m_Properties = (TPropertyTracking)Activator.CreateInstance(typeof(TPropertyTracking), this);
			AttachPropertyEvents();
		}


		/// <summary>
		/// Creates a model by auto-constructing the property bag defined by TPropertyTracking and populates it using the supplied collection
		/// </summary>
		/// <param name="collection">The collection from which the elements are copied.</param>
		protected ModelCollection(IEnumerable<T> collection)
			: base(collection)
		{
			Contract.Requires(collection != null);

			m_Properties = (TPropertyTracking)Activator.CreateInstance(typeof(TPropertyTracking), this);
			AttachPropertyEvents();
		}

		/// <summary>
		/// Returns True if there are any errors.
		/// </summary>
		/// <remarks>Call Validate() to refresh this property</remarks>
		public bool HasErrors
		{
			get { return ErrorsDictionary.HasErrors(); }
		}

		/// <summary>
		/// Returns the underlying property bag
		/// </summary>
		protected TPropertyTracking Properties
		{
			get
			{
				Contract.Ensures(Contract.Result<TPropertyTracking>() != null);

				return m_Properties;
			}
		}

		/// <summary>
		/// Returns an array of object-level errors.
		/// </summary>
		/// <remarks>Call Validate() to refresh this property.</remarks>
		[Pure]
		public ReadOnlyCollection<ValidationResult> GetErrors()
		{
			Contract.Ensures(Contract.Result<ReadOnlyCollection<ValidationResult>>() != null);
			return GetErrors("");
		}


		/// <summary>
		/// Returns an array of property-level errors.
		/// </summary>
		/// <param name="propertyName">Null or String.Empty will return the object-level errors</param>
		/// <remarks>Call Validate() to refresh this property.</remarks>
		[Pure]
		public ReadOnlyCollection<ValidationResult> GetErrors(string propertyName)
		{
			Contract.Ensures(Contract.Result<ReadOnlyCollection<ValidationResult>>() != null);
			return ErrorsDictionary.GetErrors(propertyName);
		}


		/// <summary>
		/// Invoke this method to signal the events associated with changing the errors dictionary. The parameter updateType is returned by the methods on ErrorsDictionary.
		/// </summary>
		/// <param name="propertyName"></param>
		/// <param name="updateType"></param>
		internal void OnErrorsChanged(string propertyName, ErrorsDictionaryUpdateType updateType)
		{
			if (updateType == ErrorsDictionaryUpdateType.NoChange)
				return;

			OnErrorsChanged(propertyName);

			if (updateType == ErrorsDictionaryUpdateType.HasErrorsIsFalse || updateType == ErrorsDictionaryUpdateType.HasErrorsIsTrue)
				OnPropertyChanged(CommonProperties.HasErrorsProperty);
		}

		/// <summary>
		/// Used to manually invoke the ErrorsChanged event.
		/// </summary>
		/// <param name="propertyName"></param>
		protected void OnErrorsChanged(string propertyName)
		{
			if (ErrorsChanged != null)
				ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
		}

		/// <summary>
		/// Override this method to add imperative validation at the object level.
		/// </summary>
		/// <param name="results">A collection of the declarative validation errors. You may add and remove errors from this collection.</param>
		[Pure]
		protected virtual void OnValidateObject(ValidationResultCollection results) { }

		/// <summary>
		/// Override this method to add imperative validation at the property level.
		/// </summary>
		/// <param name="propertyName">The name of the property being validated.</param>
		/// <param name="results">A collection of the declarative validation errors. You may add and remove errors from this collection.</param>
		[Pure]
		protected virtual void OnValidateProperty(string propertyName, ValidationResultCollection results) { }

		/// <summary>
		/// This forces the object to be completely revalidated.
		/// </summary>
		/// <returns>True if the object has no errors</returns>
		public bool Validate()
		{
			Properties.RevalidateAll();
			return !HasErrors;
		}


		/// <summary>
		/// Clears the error collections and the HasErrors property
		/// </summary>
		public void ClearErrors()
		{
			OnErrorsChanged("", ErrorsDictionary.Clear());
		}

		/// <summary>
		/// Fetches a value, creating it if it doesn't exist.
		/// </summary>
		/// <typeparam name="TValue">Expected type that has a parameterless constructor</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Executing the constructor will trigger validation but not a property-changed event.</remarks>
		protected TValue GetNew<TValue>([CallerMemberName] string propertyName) where TValue : new()
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null.");
			if (string.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return Properties.GetNew<TValue>(propertyName);
		}

		/// <summary>
		/// Fetches a value, creating it if it doesn't exist.
		/// </summary>
		/// <typeparam name="TValue">Expected type</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <param name="creationFunction">Function to execute if the property doesn't already exist.</param>
		/// <returns></returns>
		/// <remarks>Executing the default function will trigger validation but not a property-changed event.</remarks>
		protected TValue GetNew<TValue>(Func<TValue> creationFunction, [CallerMemberName] string propertyName)
		{
			if (creationFunction == null)
				throw new ArgumentNullException("creationFunction", "creationFunction is null.");
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null.");
			if (string.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return Properties.GetNew<TValue>(creationFunction, propertyName);
		}

		/// <summary>
		/// Fetches a value, using the default value it if it doesn't exist.
		/// </summary>
		/// <typeparam name="TValue">Expected type</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <param name="defaultValue">Default value to use</param>
		/// <returns></returns>
		/// <remarks>Storing the default value will trigger validation but not a property-changed event.</remarks>
		protected TValue Get<TValue>(TValue defaultValue, [CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null.");
			if (string.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return Properties.Get<TValue>(defaultValue, propertyName);
		}

		/// <summary>
		/// Fetches a value, returning Default(TValue) if it doesn't exist.
		/// </summary>
		/// <typeparam name="TValue">Expected type</typeparam>
		/// <param name="propertyName">Case-sensitive property name</param>
		/// <returns></returns>
		/// <remarks>Storing the default value will trigger validation but not a property-changed event.</remarks>
		protected TValue Get<TValue>([CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null.");
			if (string.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return Properties.Get<TValue>(propertyName);
		}

		///// <summary>
		///// Use this to load the model from an external datasource.
		///// </summary>
		///// <param name="fields"></param>
		///// <remarks>This would generally be called from a constructor</remarks>
		//protected virtual void Load(IDictionary<string, object> fields)
		//{
		//	if (fields == null)
		//		throw new ArgumentNullException("fields", "fields is null.");
		//	
		//	foreach (var item in fields)
		//		Properties.Set(item.Value, PropertySetModes.FixCasing | PropertySetModes.SetAsOriginal, item.Key);

		//	Properties.RevalidateAll();
		//}


		/// <summary>
		/// Set the indicated property to the value.
		/// If the value doesn't match the previous value, or if there is no previous value, raise a property changed notification.
		/// </summary>
		/// <param name="value">Value to be saved.</param>
		/// <param name="propertyName">Name of property to be created/updated</param>
		/// <exception cref="System.ArgumentOutOfRangeException">An ArgumentOutOfRangeException is thrown if System.Reflection.Missing is passed as the value.</exception>
		protected void Set(object value, [CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is null or empty.", "propertyName");
			Contract.EndContractBlock();

			Properties.Set(value, propertyName);
		}

		partial void AttributeBasedValidation(string propertyName, ValidationResultCollection results);

		//partial void AttributeBasedValidation(List<ValidationResult> results);

		void Properties_PropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			OnPropertyChanged(e);
		}

		void ValidateObject()
		{
			var results = new ValidationResultCollection();
			HashSet<string> affectedProperties;

			OnValidateObject(results);
			OnErrorsChanged("", ErrorsDictionary.SetErrors(results, out affectedProperties));

			foreach (var p in affectedProperties)
				OnErrorsChanged(p);
		}

		void ValidateProperty(string propertyName)
		{
			Contract.Requires(!String.IsNullOrEmpty(propertyName));

			var results = new ValidationResultCollection();

			AttributeBasedValidation(propertyName, results);

			OnValidateProperty(propertyName, results);

			OnErrorsChanged(propertyName, ErrorsDictionary.SetErrors(propertyName, results));
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
		[ContractInvariantMethod]
		void ObjectInvariant()
		{
			Contract.Invariant(m_Properties != null);
			Contract.Invariant(m_Errors != null);
		}

		internal ErrorsDictionary ErrorsDictionary
		{
			get
			{
				Contract.Ensures(Contract.Result<ErrorsDictionary>() != null);

				return m_Errors;
			}
		}

	}

#if NET4
	public partial class ModelCollection<T, TPropertyTracking> : IDataErrorInfo
	{
		string IDataErrorInfo.Error
		{
			get
			{
				var errors = from e in GetErrors("") select e.ToString();
				return string.Join("\n", errors.ToArray());
			}
		}

		string IDataErrorInfo.this[string columnName]
		{
			get
			{
				var errors = from e in GetErrors(columnName) select e.ToString();
				return string.Join("\n", errors.ToArray());
			}
		}



	}
#endif

#if SILVERLIGHT && !WINDOWS_PHONE
	public partial class ModelCollection<T, TPropertyTracking> : INotifyDataErrorInfo
	{

		[Pure]
		IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName)
		{
			return GetErrors(propertyName);
		}
	}
#endif

#if ATTRIBUTE_VALIDATION
	public partial class ModelCollection<T, TPropertyTracking>
	{
		//partial void AttributeBasedValidation(List<ValidationResult> results)
		//{
		//	Contract.Requires(results != null);

		//	var context = new ValidationContext(this, null, null);
		//	Validator.TryValidateObject(this, context, results);
		//}

		partial void AttributeBasedValidation(string propertyName, ValidationResultCollection results)
		{
			Contract.Requires(!string.IsNullOrEmpty(propertyName));
			Contract.Requires(results != null);

			var property = Properties.Metadata.Properties[propertyName];
			if (property.CanRead)
			{
				var context = new ValidationContext(this, null, null);
				context.MemberName = property.Name;
				Validator.TryValidateProperty(property.InvokeGet(this), context, results);
			}
		}
	}
#endif
}

